/**
 * Title: jFamilyTree provider
 * Description: jFamilyTree Provider
 * Version: provider
 * Copyright: (c) 2001-2008
 * Terms of use:see license agreement at http://treebolic.sourceforge.net/en/license.htm
 * Author: Bernard Bou
 * Company: bsys
 * Update: Mon Mar 10 00:00:00 CEST 2008
 */
package jFamilyTree.provider.xml.dom;

import java.awt.Color;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import jFamilyTree.control.Mounter;
import jFamilyTree.model.IEdge;
import jFamilyTree.model.IProvider;
import jFamilyTree.model.MenuItem;
import jFamilyTree.model.Model;
import jFamilyTree.model.MountPoint;
import jFamilyTree.model.MutableEdge;
import jFamilyTree.model.MutableNode;
import jFamilyTree.model.Settings;
import jFamilyTree.model.Tree;
import jFamilyTree.model.Utils;

/**
 * Document adapter to model/graph
 * 
 * @author Bernard Bou
 */
public class DocumentAdapter
{
	// D A T A

	/**
	 * Mount task (protracted)
	 * 
	 * @author Bernard Bou
	 */
	class MountTask
	{
		/**
		 * Mount point
		 */
		public MountPoint.Mounting theMountPoint;

		/**
		 * Node to mount graph at
		 */
		public MutableNode theMountingNode;

		/**
		 * Constructor
		 * 
		 * @param thisMountPoint
		 *        mount point
		 * @param thisMountingNode
		 *        Node to mount graph at
		 */
		public MountTask(MountPoint.Mounting thisMountPoint, MutableNode thisMountingNode)
		{
			theMountPoint = thisMountPoint;
			theMountingNode = thisMountingNode;
		}

		/**
		 * Perform task
		 * 
		 * @param theseEdges
		 *        edges in grafting tree
		 */
		public void run(List<IEdge> theseEdges)
		{
			if (theProvider == null)
			{
				System.out.println("Mount not performed: " + theMountPoint + " @ " + theMountingNode);
				return;
			}
			Tree thisTree = theProvider.makeTree(theMountPoint.theURL, true);
			if (thisTree != null)
				Mounter.graft(theMountingNode, thisTree.getRoot(), theseEdges, thisTree.getEdges());
		}
	}

	/**
	 * Protracted mount tasks
	 */
	private List<MountTask> theMountTasks = null;

	/**
	 * Provider (used to generate mounted trees)
	 */
	private IProvider theProvider;

	/**
	 * Id to node map
	 */
	private Map<String, MutableNode> theIdToNodeMap;

	// C O N S T R U C T O R

	/**
	 * Constructor
	 * 
	 * @param thisProvider
	 *        provider (used in recursion)
	 */
	public DocumentAdapter(IProvider thisProvider)
	{
		theProvider = thisProvider;
	}

	/**
	 * Constructor
	 */
	public DocumentAdapter()
	{
		this(null);
	}

	// A C C E S S

	/**
	 * Get id to node map
	 * 
	 * @return id to node map
	 */
	public Map<String, MutableNode> getIdToNodeMap()
	{
		return theIdToNodeMap;
	}

	// M A K E

	/**
	 * Make model from document
	 * 
	 * @param thisDocument
	 *        document
	 * @return model
	 */
	public Model makeModel(Document thisDocument)
	{
		theIdToNodeMap = new TreeMap<String, MutableNode>();
		Tree thisTree = toTree(thisDocument);
		if (thisTree == null)
			return null;
		Settings theseSettings = toSettings(thisDocument);
		return new Model(thisTree, theseSettings);
	}

	/**
	 * Make tree from document
	 * 
	 * @param thisDocument
	 *        document
	 * @return tree
	 */
	public Tree makeTree(Document thisDocument)
	{
		theIdToNodeMap = new Hashtable<String, MutableNode>();
		Tree thisTree = toTree(thisDocument);
		return thisTree;
	}

	// P A R S E

	/**
	 * Make model node
	 * 
	 * @param thisParent
	 *        model parent
	 * @param thisId
	 *        id
	 * @return model node
	 */
	protected MutableNode makeNode(MutableNode thisParent, String thisId)
	{
		return new MutableNode(thisParent, thisId);
	}

	/**
	 * Make model edge
	 * 
	 * @param thisFromNode
	 *        model from-node end
	 * @param thisToNode
	 *        model to-node end
	 * @return model edge
	 */
	protected MutableEdge makeEdge(MutableNode thisFromNode, MutableNode thisToNode)
	{
		return new MutableEdge(thisFromNode, thisToNode);
	}

	/**
	 * Make graph
	 * 
	 * @param thisDocument
	 *        document
	 * @return graph
	 */
	private Tree toTree(Document thisDocument)
	{
		// nodes
		Element thisRootElement = getFirstElementByTagName(thisDocument.getDocumentElement(), "node");
		if (thisRootElement == null)
			return null;
		MutableNode thisRoot = toNode(thisRootElement, null);

		// edges
		List<IEdge> theseEdges = toEdges(thisDocument);

		// run protracted mount tasks (had to be protracted until edges become available)
		if (theMountTasks != null)
		{
			for (MountTask thisTask : theMountTasks)
				thisTask.run(theseEdges);
			theMountTasks.clear();
			theMountTasks = null;
		}

		return new Tree(thisRoot, theseEdges);
	}

	/**
	 * Make model node
	 * 
	 * @param thisNodeElement
	 *        starting DOM element
	 * @param thisParent
	 *        model parent node
	 * @return model node
	 */
	private MutableNode toNode(Element thisNodeElement, MutableNode thisParent)
	{
		// id
		String thisId = thisNodeElement.getAttribute("id");

		// make
		MutableNode thisNode = makeNode(thisParent, thisId);
		theIdToNodeMap.put(thisId, thisNode);

		// colors
		Color thisBackColor = Utils.stringToColor(thisNodeElement.getAttribute("backcolor"));
		thisNode.setBackColor(thisBackColor);
		Color thisForeColor = Utils.stringToColor(thisNodeElement.getAttribute("forecolor"));
		thisNode.setForeColor(thisForeColor);

		// weight
		String thisWeight = thisNodeElement.getAttribute("weight");
		if (thisWeight != null && !thisWeight.isEmpty())
			thisNode.setWeight(-Double.parseDouble(thisWeight));

		// label
		Element thisElement = getFirstLevel1ElementByTagName(thisNodeElement, "label");
		if (thisElement != null)
		{
			String thisLabel = thisElement.getTextContent();
			if (thisLabel != null && !thisLabel.isEmpty())
				thisNode.setLabel(thisLabel);
		}

		// content
		thisElement = getFirstLevel1ElementByTagName(thisNodeElement, "content");
		if (thisElement != null)
		{
			String thisContent = thisElement.getTextContent();
			if (thisContent != null && !thisContent.isEmpty())
				thisNode.setContent(thisContent);
		}

		// image
		thisElement = getFirstLevel1ElementByTagName(thisNodeElement, "img");
		if (thisElement != null)
		{
			String thisImageSrc = thisElement.getAttribute("src");
			if (thisImageSrc != null && !thisImageSrc.isEmpty())
				thisNode.setImageFile(thisImageSrc);
		}

		// tree.edge
		thisElement = getFirstLevel1ElementByTagName(thisNodeElement, "treeedge");
		if (thisElement != null)
		{
			// label
			Element thisLabelElement = getFirstLevel1ElementByTagName(thisElement, "label");
			if (thisLabelElement != null)
			{
				String thisLabel = thisLabelElement.getTextContent();
				if (thisLabel != null && !thisLabel.isEmpty())
					thisNode.setEdgeLabel(thisLabel);
			}

			// image
			Element thisImageElement = getFirstLevel1ElementByTagName(thisElement, "img");
			if (thisImageElement != null)
			{
				String thisImageSrc = thisImageElement.getAttribute("src");
				if (thisImageSrc != null && !thisImageSrc.isEmpty())
					thisNode.setEdgeImageFile(thisImageSrc);
			}

			// color
			Color thisColor = Utils.stringToColor(thisElement.getAttribute("color"));
			if (thisColor != null)
				thisNode.setEdgeColor(thisColor);

			// style
			Integer thisStyle = Utils.parseStyle(thisElement.getAttribute("stroke"), thisElement.getAttribute("fromterminator"), thisElement.getAttribute("toterminator"), thisElement.getAttribute("hidden"));
			if (thisStyle != null)
				thisNode.setEdgeStyle(thisStyle);
		}

		// link
		thisElement = getFirstLevel1ElementByTagName(thisNodeElement, "a");
		if (thisElement != null)
		{
			String thisHref = thisElement.getAttribute("href");
			if (thisHref != null && !thisHref.isEmpty())
				thisNode.setLink(thisHref);
		}

		// mount
		thisElement = getFirstLevel1ElementByTagName(thisNodeElement, "mountpoint");
		if (thisElement != null)
		{
			Element thisAElement = getFirstElementByTagName(thisElement, "a");
			if (thisAElement != null)
			{
				String thisHref = thisAElement.getAttribute("href");
				if (thisHref != null && !thisHref.isEmpty())
				{
					MountPoint.Mounting thisMountPoint = new MountPoint.Mounting();
					thisMountPoint.theURL = thisHref;

					// mount now ?
					String thisValue = thisElement.getAttribute("now");
					if (thisValue != null && !thisValue.isEmpty() && Boolean.valueOf(thisValue))
					{
						thisMountPoint.now = true;

						MountTask thisTask = new MountTask(thisMountPoint, thisNode);
						if (theMountTasks == null)
							theMountTasks = new ArrayList<MountTask>();
						theMountTasks.add(thisTask);
					}
					thisNode.setMountPoint(thisMountPoint);
				}
			}
		}

		// recurse to children
		List<Element> theseChildElements = getLevel1ChildElementsByTagName(thisNodeElement, "node");
		for (Element thisChildElement : theseChildElements)
			toNode(thisChildElement, thisNode);

		return thisNode;
	}

	/**
	 * Make model edge
	 * 
	 * @param thisEdgeElement
	 *        edge DOM element
	 * @return edge
	 */
	private MutableEdge toEdge(Element thisEdgeElement)
	{
		String thisFromId = thisEdgeElement.getAttribute("from");
		String thisToId = thisEdgeElement.getAttribute("to");
		MutableNode thisFromNode = theIdToNodeMap.get(thisFromId);
		MutableNode thisToNode = theIdToNodeMap.get(thisToId);
		MutableEdge thisEdge = makeEdge(thisFromNode, thisToNode);

		// label
		Element thisLabelElement = getFirstLevel1ElementByTagName(thisEdgeElement, "label");
		if (thisLabelElement != null)
		{
			String thisLabel = thisLabelElement.getTextContent();
			if (thisLabel != null && !thisLabel.isEmpty())
				thisEdge.setLabel(thisLabel);
		}

		// image
		Element thisImageElement = getFirstLevel1ElementByTagName(thisEdgeElement, "img");
		if (thisImageElement != null)
		{
			String thisImageSrc = thisImageElement.getAttribute("src");
			if (thisImageSrc != null && !thisImageSrc.isEmpty())
				thisEdge.setImageFile(thisImageSrc);
		}

		// color
		Color thisColor = Utils.stringToColor(thisEdgeElement.getAttribute("color"));
		if (thisColor != null)
			thisEdge.setColor(thisColor);

		// style
		Integer thisStyle = Utils.parseStyle(thisEdgeElement.getAttribute("stroke"), thisEdgeElement.getAttribute("fromterminator"), thisEdgeElement.getAttribute("toterminator"), thisEdgeElement.getAttribute("hidden"));
		if (thisStyle != null)
			thisEdge.setStyle(thisStyle);
		return thisEdge;
	}

	/**
	 * Make list of model edges
	 * 
	 * @param thisDocument
	 *        DOM document
	 * @return list of edges
	 */
	private List<IEdge> toEdges(Document thisDocument)
	{
		List<IEdge> thisEdgeList = null;
		NodeList theseChildren = thisDocument.getElementsByTagName("edge");
		for (int i = 0; i < theseChildren.getLength(); i++)
		{
			Node thisNode = theseChildren.item(i);
			Element thisEdgeElement = (Element) thisNode;
			MutableEdge thisEdge = toEdge(thisEdgeElement);
			if (thisEdgeList == null)
				thisEdgeList = new ArrayList<IEdge>();
			thisEdgeList.add(thisEdge);
		}
		return thisEdgeList;
	}

	/**
	 * Make settings
	 * 
	 * @param thisDocument
	 *        DOM document
	 * @return settings
	 */
	static private Settings toSettings(Document thisDocument)
	{
		Settings theseSettings = new Settings();

		// T O P
		Element thisElement = thisDocument.getDocumentElement();
		if (thisElement != null && thisElement.getNodeName().equals("jFamilyTree"))
		{
			String thisAttribute = thisElement.getAttribute("toolbar");
			if (thisAttribute != null && !thisAttribute.isEmpty())
				theseSettings.theHasToolbarFlag = Boolean.valueOf(thisAttribute);
			thisAttribute = thisElement.getAttribute("statusbar");
			if (thisAttribute != null && !thisAttribute.isEmpty())
				theseSettings.theHasStatusbarFlag = Boolean.valueOf(thisAttribute);
			thisAttribute = thisElement.getAttribute("popupmenu");
			if (thisAttribute != null && !thisAttribute.isEmpty())
				theseSettings.theHasPopUpMenuFlag = Boolean.valueOf(thisAttribute);
			thisAttribute = thisElement.getAttribute("tooltip");
			if (thisAttribute != null && !thisAttribute.isEmpty())
				theseSettings.theHasToolTipFlag = Boolean.valueOf(thisAttribute);
			thisAttribute = thisElement.getAttribute("tooltip-displays-content");
			if (thisAttribute != null && !thisAttribute.isEmpty())
				theseSettings.theToolTipDisplaysContentFlag = Boolean.valueOf(thisAttribute);
			thisAttribute = thisElement.getAttribute("focus");
			if (thisAttribute != null && !thisAttribute.isEmpty())
				theseSettings.theFocus = thisAttribute;
			thisAttribute = thisElement.getAttribute("focus-on-hover");
			if (thisAttribute != null && !thisAttribute.isEmpty())
				theseSettings.theFocusOnHoverFlag = Boolean.valueOf(thisAttribute);
			thisAttribute = thisElement.getAttribute("xmoveto");
			if (thisAttribute != null && !thisAttribute.isEmpty())
				theseSettings.theXMoveTo = Double.valueOf(thisAttribute).doubleValue();
			thisAttribute = thisElement.getAttribute("ymoveto");
			if (thisAttribute != null && !thisAttribute.isEmpty())
				theseSettings.theYMoveTo = Double.valueOf(thisAttribute).doubleValue();
			thisAttribute = thisElement.getAttribute("xshift");
			if (thisAttribute != null && !thisAttribute.isEmpty())
				theseSettings.theXShift = Double.valueOf(thisAttribute).doubleValue();
			thisAttribute = thisElement.getAttribute("yshift");
			if (thisAttribute != null && !thisAttribute.isEmpty())
				theseSettings.theYShift = Double.valueOf(thisAttribute).doubleValue();
		}

		// T R E E
		thisElement = getFirstElementByTagName(thisDocument.getDocumentElement(), "tree");
		if (thisElement != null)
		{
			// img
			Element thisImageElement = getFirstLevel1ElementByTagName(thisElement, "img");
			if (thisImageElement != null)
			{
				String thisSrc = thisImageElement.getAttribute("src");
				if (thisSrc != null && !thisSrc.isEmpty())
					theseSettings.theBackgroundImage = thisSrc;
			}

			// colors
			Color thisColor = Utils.stringToColor(thisElement.getAttribute("backcolor"));
			if (thisColor != null)
				theseSettings.theBackColor = thisColor;
			thisColor = Utils.stringToColor(thisElement.getAttribute("forecolor"));
			if (thisColor != null)
				theseSettings.theForeColor = thisColor;

			// attributes
			String thisAttribute;
			thisAttribute = thisElement.getAttribute("orientation");
			if (thisAttribute != null && !thisAttribute.isEmpty())
				theseSettings.theOrientation = thisAttribute;
			thisAttribute = thisElement.getAttribute("expansion");
			if (thisAttribute != null && !thisAttribute.isEmpty())
				theseSettings.theExpansion = Double.valueOf(thisAttribute).doubleValue();
			thisAttribute = thisElement.getAttribute("sweep");
			if (thisAttribute != null && !thisAttribute.isEmpty())
				theseSettings.theSweep = Double.valueOf(thisAttribute).doubleValue();
			thisAttribute = thisElement.getAttribute("preserve-orientation");
			if (thisAttribute != null && !thisAttribute.isEmpty())
				theseSettings.thePreserveOrientationFlag = Boolean.valueOf(thisAttribute);
			thisAttribute = thisElement.getAttribute("fontface");
			if (thisAttribute != null && !thisAttribute.isEmpty())
				theseSettings.theFontFace = thisAttribute;
			thisAttribute = thisElement.getAttribute("fontsize");
			if (thisAttribute != null && !thisAttribute.isEmpty())
				theseSettings.theFontSize = Integer.valueOf(thisAttribute).intValue();
			thisAttribute = thisElement.getAttribute("fontsizestep");
			if (thisAttribute != null && !thisAttribute.isEmpty())
				theseSettings.theFontSizeStep = Integer.valueOf(thisAttribute).intValue();
		}

		// N O D E S
		thisElement = getFirstElementByTagName(thisDocument.getDocumentElement(), "nodes");
		if (thisElement != null)
		{
			// img
			Element thisImageElement = getFirstLevel1ElementByTagName(thisElement, "img");
			if (thisImageElement != null)
			{
				String thisSrc = thisImageElement.getAttribute("src");
				if (thisSrc != null && !thisSrc.isEmpty())
					theseSettings.theDefaultNodeImage = thisSrc;
			}

			// colors
			Color thisColor = Utils.stringToColor(thisElement.getAttribute("backcolor"));
			if (thisColor != null)
				theseSettings.theNodeBackColor = thisColor;
			thisColor = Utils.stringToColor(thisElement.getAttribute("forecolor"));
			if (thisColor != null)
				theseSettings.theNodeForeColor = thisColor;
		}

		// E D G E S
		thisElement = getFirstElementByTagName(thisDocument.getDocumentElement(), "edges");
		if (thisElement != null)
		{
			// img
			Element thisImageElement = getFirstLevel1ElementByTagName(thisElement, "img");
			if (thisImageElement != null)
			{
				String thisSrc = thisImageElement.getAttribute("src");
				if (thisSrc != null && !thisSrc.isEmpty())
					theseSettings.theDefaultEdgeImage = thisSrc;
			}

			// hyperbolic edges
			String thisAttribute = thisElement.getAttribute("hyperbolic");
			if (thisAttribute != null && !thisAttribute.isEmpty())
				theseSettings.theHyperbolicEdgesFlag = Boolean.valueOf(thisAttribute);
		}

		// D E F A U L T . T R E E . E D G E
		thisElement = getFirstElementByTagName(thisDocument.getDocumentElement(), "default.treeedge");
		if (thisElement != null)
		{
			// img
			Element thisImageElement = getFirstElementByTagName(thisElement, "img");
			if (thisImageElement != null)
			{
				String thisSrc = thisImageElement.getAttribute("src");
				if (thisSrc != null && !thisSrc.isEmpty())
					theseSettings.theDefaultTreeEdgeImage = thisSrc;
			}

			// color
			Color thisColor = Utils.stringToColor(thisElement.getAttribute("color"));
			if (thisColor != null)
				theseSettings.theTreeEdgeColor = thisColor;

			// style
			Integer thisStyle = Utils.parseStyle(thisElement.getAttribute("stroke"), thisElement.getAttribute("fromterminator"), thisElement.getAttribute("toterminator"), thisElement.getAttribute("hidden"));
			if (thisStyle != null)
				theseSettings.theTreeEdgeStyle = thisStyle;
		}

		// D E F A U L T . E D G E
		thisElement = getFirstElementByTagName(thisDocument.getDocumentElement(), "default.edge");
		if (thisElement != null)
		{
			// img
			Element thisImageElement = getFirstElementByTagName(thisElement, "img");
			if (thisImageElement != null)
			{
				String thisSrc = thisImageElement.getAttribute("src");
				if (thisSrc != null && !thisSrc.isEmpty())
					theseSettings.theDefaultEdgeImage = thisSrc;
			}

			// color
			Color thisColor = Utils.stringToColor(thisElement.getAttribute("color"));
			if (thisColor != null)
				theseSettings.theEdgeColor = thisColor;

			// style
			Integer thisStyle = Utils.parseStyle(thisElement.getAttribute("stroke"), thisElement.getAttribute("fromterminator"), thisElement.getAttribute("toterminator"), thisElement.getAttribute("hidden"));
			if (thisStyle != null)
				theseSettings.theEdgeStyle = thisStyle;
		}

		// D E F A U L T . E D G E
		List<MenuItem> thisMenuItemList = null;
		NodeList theseChildren = thisDocument.getElementsByTagName("menuitem");
		for (int i = 0; i < theseChildren.getLength(); i++)
		{
			Node thisNode = theseChildren.item(i);
			thisElement = (Element) thisNode;
			MenuItem thisMenuItem = toMenuItem(thisElement);

			if (thisMenuItemList == null)
				thisMenuItemList = new ArrayList<MenuItem>();
			thisMenuItemList.add(thisMenuItem);
		}
		theseSettings.theMenu = thisMenuItemList;

		return theseSettings;
	}

	/**
	 * Make menu item
	 * 
	 * @param thisElement
	 *        menu item DOM element
	 * @return menu item
	 */
	static private MenuItem toMenuItem(Element thisElement)
	{
		MenuItem thisMenuItem = new MenuItem();
		Utils.parseMenuItem(thisMenuItem, thisElement.getAttribute("action"), thisElement.getAttribute("match-scope"), thisElement.getAttribute("match-mode"));

		// match target
		String thisAttribute = thisElement.getAttribute("match-target");
		thisMenuItem.theMatchTarget = thisAttribute;

		// label
		Element thisLabelElement = getFirstElementByTagName(thisElement, "label");
		if (thisLabelElement != null)
			thisMenuItem.theLabel = thisLabelElement.getTextContent();

		// link
		Element thisLinkElement = getFirstElementByTagName(thisElement, "a");
		if (thisLinkElement != null)
		{
			thisMenuItem.theLink = thisLinkElement.getAttribute("href");
			thisMenuItem.theTarget = thisLinkElement.getAttribute("target");
		}

		return thisMenuItem;
	}

	// H E L P E R S

	/**
	 * Find DOM element with given tag
	 * 
	 * @param thisElement
	 *        starting DOM element
	 * @param thisTagName
	 *        tag
	 * @return DOM element if found, null if none
	 */
	static private Element getFirstElementByTagName(Element thisElement, String thisTagName)
	{
		if (thisElement != null)
		{
			NodeList thisList = thisElement.getElementsByTagName(thisTagName);
			if (thisList.getLength() > 0)
				return (Element) thisList.item(0);
		}
		return null;
	}

	/**
	 * Find DOM element with given tag among first level children
	 * 
	 * @param thisElement
	 *        starting DOM element
	 * @param thisTagName
	 *        tag
	 * @return DOM element if found, null if none
	 */
	static private Element getFirstLevel1ElementByTagName(Element thisElement, String thisTagName)
	{
		List<Element> theseChildElements = getLevel1ChildElementsByTagName(thisElement, thisTagName);
		if (!theseChildElements.isEmpty())
			return theseChildElements.get(0);
		return null;
	}

	/**
	 * Find DOM elements with given tag among first level children
	 * 
	 * @param thisElement
	 *        starting DOM element
	 * @param thisTagName
	 *        tag
	 * @return DOM element if found, null if none
	 */
	static private List<Element> getLevel1ChildElementsByTagName(Element thisElement, String thisTagName)
	{
		ArrayList<Element> thisList = new ArrayList<Element>();
		NodeList theseChildren = thisElement.getChildNodes();
		for (int i = 0; i < theseChildren.getLength(); i++)
		{
			Node thisNode = theseChildren.item(i);
			if (thisNode instanceof Element)
			{
				Element thisChildElement = (Element) thisNode;
				if (thisChildElement.getTagName().equals(thisTagName))
					thisList.add(thisChildElement);
			}
		}
		return thisList;
	}
}
